module sw.admin.adminmodelist;
import nazo.date, nazo.http, std.uri, std.string, std.conv, std.stdio, std.c.process, nazo.file, nazo.process, std.contracts, nazo.license, nazo.xhtml, nazo.env;
import sw.mode, sw.contentmode, sw.cgiinfo, sw.siteconfig, sw.unsupportedmode, sw.locale.locale, sw.client, sw.synchronize, sw.modelist, sw.utils, sw.menuitem, sw.main;
alias std.string.toString toString;

class AdminModeFactory:ModeFactory{
  mixin TModeFactory!();
  const static string prefix="admin";
  IMode getMode(string name){
    switch(name){
      case "login":
        return new LoginMode(cfg);
      case "logincheck":
        return new LoginCheckMode(cfg);
      case "setting":
        return new SettingMode(cfg);
      case "set":
        return new SetMode(cfg);
      case "sync":
        return new SyncMode(cfg);
      case "logout":
        return new LogoutMode(cfg);
      case "changepass":
        return new ChangePasswordMode(cfg);
      case "updatepass":
        return new UpdatePasswordMode(cfg);
      case "shell":
        return new ShellMode(cfg);
      case "exec":
        return new ExecMode(cfg);
      default:
    }
    return new UnsupportedMode(cfg);
  }
}

class LoginCheckMode:LoginMode{
  override d_time lastModified(){
    return 0;//二種類ある
  }
  bool isOK;
  override string title(){if(!isOK)return CGI.locale.getValue!(Locale.Page.loginFailed)();return CGI.locale.getValue!(Locale.Page.logined)();}
  this(SiteConfig* cfg){
    super(cfg);
    isOK=cast(bool)(Client.authority==Client.Authority.Admin);
  }
  override string contentBody(){
    if(isOK){
      CreateMenuItem cmi=CreateMenuItem(cfg);
      Menu[2] menu;
      cmi.create!("admin:setting")(&menu[0]);
      string s;
      version(Wiki){
        cmi.create!("wiki:index")(&menu[1]);
        s="<li>"~menu[1].content~"</li>\n";
      }
      return "<p>"~CGI.locale.getValue!(Locale.Page.loginedDescription)()~"</p><ul>\n"~s~"<li>"~menu[0].content~"</li>\n</ul>";
    }
    string rv;
    rv~="<p>"~CGI.locale.getValue!(Locale.Page.loginFailedDescription)()~"</p>";
    return rv ~ (cast(LoginMode)super).contentBody();
  }
}

class LoginMode:XhtmlMode!(){
  override d_time lastModified(){
    return (new CgiInfo).datetime;//二種類あるがuriが異なるのでok
  }
  bool isLogined=false;
  this(SiteConfig* cfg){
    super(cfg);
    isLogined=cast(bool)(Client.authority==Client.Authority.Admin);
    if(!isLogined){
      this.scripts~=Http.basePath~"md5.js";
      this.scripts~=Http.basePath~"login.js";
    }
    this.useJSCookie = true;
  }
  override int permission(){
    if(cfg.personalMode)
      return Client.Authority.Admin | Client.Authority.Bot |
             Client.Authority.Guest;
    else
      return Client.Authority.Admin | Client.Authority.Guest;
  }
  override string title(){return CGI.locale.getValue!(Locale.Page.login)();}
  override string contentBody(){
    string result;
    CreateMenuItem cmi=CreateMenuItem(cfg);
    if(isLogined){
      Menu[2] menu;
      cmi.create!("admin:setting")(&menu[0]);
      string s;
      version(Wiki){
        cmi.create!("wiki:index")(&menu[1]);
        s="<li>"~menu[1].content~"</li>\n";
      }
      return "<p>既にログインしています。</p><ul>\n"~s~"<li>"~menu[0].content~"</li>\n</ul>";///FIXME: 未ローカライズ
    }
    string s;
    version(Wiki){
      Menu[1] menu;
      cmi.createWithId!(Locale.Menu.cancel)(&menu[0], "wiki:index");
      s = menu[0].content;
    }
    result~="<p>\n<input type=\"hidden\" id=\"salt\" value=\""~Client.getSalt~"\"/>\n"
            "<label>"~CGI.locale.getValue!(Locale.Page.loginLabel)()~"<input type=\"password\" id=\"password\"/></label>\n"
            "<input type=\"button\" value=\""~CGI.locale.getValue!(Locale.Page.loginValue)()~"\" onclick=\"login();\"/>\n"
            ~ s ~
            "</p>";
    return result;
  }
}

///Bugs: 未ローカライズ
class SettingMode:XhtmlMode!(){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override int permission(){
      return Client.Authority.Admin;
  }
  override string title(){return "コントロールパネル";}
  override string contentBody(){
    string form;
    form~="<div class=\"section\"><h3>情報</h3><div class=\"about\">";
    form~="<dl><dt>ディスク使用量</dt><dd>"~.toString(cfg.usingSize)~" bytes</dd></dl>";
    ///Bugs: 微妙に数が異なる
    form~="<dl><dt>総ファイル数</dt><dd>"~exec("find . | wc -l")~"</dd></dl>";
    form~="<dl><dt>ユーザー情報</dt><dd>"~exec("id")~"</dd></dl>";
    form~="<dl><dt>OS情報</dt><dd>"~exec("cat /proc/version")~"</dd></dl>";
    form~="<dl><dt>CPU情報</dt><dd>"~exec("cat /proc/cpuinfo | grep model\\ name | sed -e \"s/model name//g\" | nl -v 0").replace("\n","<br/>")~"</dd></dl>";
//    form~="<dl><dt>平均負荷</dt><dd>"~exec("cat /proc/loadavg")~"</dd></dl>";
//    form~="<dl><dt>プロセス</dt><dd>"~exec("ps -e | grep index.cgi").replace("<","&lt;").replace(">","&gt;").replace("\n","<br/>")~"</dd></dl>";
//    form~="<dl><dt>top -b -n1</dt><dd>"~exec("top -b -n1").replace("<","&lt;").replace(">","&gt;").replace("\n","<br/>")~"</dd></dl>";
//    form~="<dl><dt></dt><dd>"~exec("cat /proc/`ps | grep index.cgi | sed -e \"s/\\s.*//\"`/cmdline").replace("\n","<br/>")~"</dd></dl>";
//    form~="<dl><dt>メモリ使用量</dt><dd>"~exec("free")~"</dd></dl>";
    form~="<dl><dt>情報色々</dt><dd>"~exec("top -b -n1 -u `whoami`").replace("<","&lt;").replace(">","&gt;").replace("\n","<br/>")~"</dd></dl>";

    form~="</div></div>";
    form~="<div class=\"section\"><h3>コマンド</h3><div class=\"about\">";
    form~="<form action=\""~Http.relPath~"\" method=\"post\"><input type=\"hidden\" name=\"mode\" value=\"admin:logout\"/><button type=\"submit\">ログアウト</button></form>";
    if(cfg.synchronizeURI!=""){
      form~="<form action=\""~Http.relPath~"\" method=\"post\"><input type=\"hidden\" name=\"mode\" value=\"admin:sync\"/><button type=\"submit\">今すぐ同期</button></form>";
    }
    form~="<p><a href=\""~Http.relPath~"?mode=admin:shell\">シェル</a></p>";
    form~="<p><a href=\""~Http.relPath~"?mode=admin:changepass\">パスワードの変更</a></p>";
    form~="</div></div>";
    form~="<div class=\"section\"><h3>設定</h3><div class=\"about\">";
    form~=`<form action="`~Http.relPath~`" method="post">`\n`<input type="hidden" name="mode" value="admin:set"/>`\n;
    auto cfg = this.cfg;
    void add(string T, U=int)(string localizedName, U options = U.init){
      static if(is(typeof(mixin(T)):string)){
        static if(is(U:bool)){
          if(options == true)
            form~=`<label>`~localizedName~`:<textarea name="`~T~`">`~mixin(T)~`</textarea></label><br/>`\n;
          else
            form~=`<label>`~localizedName~`:<input type="text" name="`~T~`" value="`~mixin(T)~`"/></label><br/>`\n;
        }else
          form~=`<label>`~localizedName~`:<input type="text" name="`~T~`" value="`~mixin(T)~`"/></label><br/>`\n;
      }else static if(is(typeof(mixin(T)):bool))
        form~=`<label>`~localizedName~`:<input type="checkbox" name="`~T~`"`~(mixin(T)?` checked="checked"`:"")~`/></label><br/>`\n;
      else static if(is(typeof(mixin(T)):int)){
        static if(is(U:string[])){
          if(options.length){
            form~=`<label>`~localizedName~`:<select name="`~T~`">`;
            foreach(uint i,string label;options){
              form~=`<option value="`~.toString(i)~(mixin(T)==i?`" selected="selected`:``)~`">`~label~`</option>`;
            }
            form~=`</select></label><br/>`\n;
          }else
            form~=`<label>`~localizedName~`:<input type="text" name="`~T~`" value="`~.toString(mixin(T))~`"/></label><br/>`\n;
        }else
          form~=`<label>`~localizedName~`:<input type="text" name="`~T~`" value="`~.toString(mixin(T))~`"/></label><br/>`\n;
      }else
        static assert(0);
    }
    
    if(cfg.synchronizeURI==""){
      add!("cfg.siteName")("サイトの名前");
      add!("cfg.siteDescription")("サイトの説明");
      add!("cfg.defaultLanguage")("サイトの言語");
      add!("cfg.manager")("管理人の名前");
      add!("cfg.mailAddr")("メールアドレス");
      add!("cfg.cascadingStyleSheet")("CSSのURI");
      add!("cfg.favIcon")("サイトのアイコンのURI");
      add!("cfg.header", bool)("ヘッダ", true);
      add!("cfg.footer", bool)("フッタ", true);
    }
    form~="<div class=\"section\"><h4>同期</h4><div class=\"about\">";
    add!("cfg.synchronizeURI")("同期元URI(しないなら空)");
    form~="</div></div>";
    if(cfg.synchronizeURI==""){
      version(Wiki){
        form~="<div class=\"section\"><h4>Wiki</h4><div class=\"about\">";
        add!("cfg.topPageName")("トップページの名前");
        add!("cfg.usePathInfo")("PathInfo環境変数を使う");
        add!("cfg.useLastModified")("Last-Modifiedヘッダを使う");
        add!("cfg.personalMode")("個人モードにする");
        add!("cfg.canNewPage")("一般ユーザが新しいページを作成するのを許可する");
        auto licenses = LicenseFactory.items();
        string[] licensenames;
        licensenames.length = licenses.length;
        foreach(i,license;licenses)
          licensenames[i] = license.name();
        add!("cfg.wikiDocumentLicense", string[])("文章のライセンス(容易に変えないでください)", licensenames);
        form~="</div></div>";
      }
      version(Upload){
        form~="<div class=\"section\"><h4>アップローダー</h4><div class=\"about\">";
        add!("cfg.defaultItemPerPage")("標準時の一ページに表示するアイテムの個数");
        add!("cfg.uploadMaxSize")("アップロードできるファイルの最大サイズ(単位はKiB)");
        add!("cfg.uploadMaxItem")("保存しておくファイルの最大個数(0なら制限無し)");
        form~="</div></div>";
      }
    }
    string s;
    version(Wiki){
      CreateMenuItem cmi=CreateMenuItem(cfg);
      Menu[1] menu;
      cmi.createWithId!(Locale.Menu.cancel)(&menu[0], "wiki:index");
      s = menu[0].content;
    }
    form~=`<input type="submit" value="設定"/>`\n
          ~ s ~
          `</form>`;
    form~="</div></div>";
    return form;
  }
}

///Bugs: 未ローカライズ
class SetMode:XhtmlMode!(ModeMethod.POST){
  override int permission(){
      return Client.Authority.Admin;
  }
  this(SiteConfig* cfg){
    super(cfg);

    void append(T)(ref T x, T val){
      x = val;
    }
    void get(alias T)(){
      if(auto t=T in Http.query){
        static if(is(typeof(mixin(T)):string))
          append(mixin(T), uridecode(*t));
        else static if(is(typeof(mixin(T)) == bool))
          append(mixin(T), *t?true:false);
        else static if(is(typeof(mixin(T)) == uint))
          append(mixin(T), .toUint(uridecode(*t)));
        else static assert(0);
        return;
      }
      append(mixin(T), /*typeof(*/mixin(T)/*)*/.init);
    }

    if(cfg.synchronizeURI==""){
      get!("cfg.siteName" /*cfg.siteName.stringof*/ );
      get!("cfg.siteDescription");
      get!("cfg.defaultLanguage");
      get!("cfg.manager");
      get!("cfg.mailAddr");
      get!("cfg.cascadingStyleSheet");
      get!("cfg.favIcon");
      get!("cfg.header");
      get!("cfg.footer");

      version(Wiki){
        get!("cfg.topPageName");
        get!("cfg.usePathInfo");
        get!("cfg.useLastModified");
        get!("cfg.personalMode");
        get!("cfg.canNewPage");
        get!("cfg.wikiDocumentLicense");
      }
      version(Upload){
        get!("cfg.defaultItemPerPage");
        get!("cfg.uploadMaxSize");
        get!("cfg.uploadMaxItem");
      }
    }
    version(Wiki)
      get!("cfg.synchronizeURI");
    cfg.flash();
  }
  override string title(){return "設定完了";}
  override string contentBody(){
    CreateMenuItem cmi=CreateMenuItem(cfg);
    Menu[2] menu;
    cmi.create!("admin:setting")(&menu[0]);
    string s;
    version(Wiki){
      cmi.create!("wiki:index")(&menu[1]);
      s="<li>"~menu[1].content~"</li>\n";
    }
    return "<p>設定が完了しました。</p><ul>\n"~s~"<li>"~menu[0].content~"</li>\n</ul>";
  }
}

class SyncMode:ContentMode!(ModeMethod.POST){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override int permission(){
    return Client.Authority.Admin;
  }
  override string filename(){
    return "synchronize.log";
  }
  override string contentType(){
    return "text/plain";
  }
  override string content(){
    if(cfg.synchronizeURI == "")throw new SecurityException;
    writefln(\n);
    synchronize(cfg);
    exit(0);
    return null;
  }
}

///FIXME: 未ローカライズ
class LogoutMode:XhtmlMode!(ModeMethod.POST){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override int permission(){
      return Client.Authority.Admin;
  }
  override string title(){return "ログアウト";}
  override string contentBody(){
    headers["Set-Cookie"] = "password=; expires=Fri, 31-Dec-1999 23:59:59 GMT";
    Client.authority=Client.Authority.Guest;

    CreateMenuItem cmi=CreateMenuItem(cfg);
    Menu menu;
    string s;
    version(Wiki){
      cmi.create!("wiki:index")(&menu);
      s="<li>"~menu.content~"</li>\n";
    }
    return "<p>ログアウトしました。</p><ul>\n"~s~"</ul>";
  }
}

///FIXME: 未ローカライズ
class ChangePasswordMode:XhtmlMode!(){
  override int permission(){
      return Client.Authority.Admin;
  }
  this(SiteConfig* cfg){
    super(cfg);
    this.scripts~=Http.basePath~"md5.js";
    this.scripts~=Http.basePath~"login.js";
    this.useJSCookie = true;
  }
  override string title(){return "パスワードの変更";}
  override string contentBody(){
    string rv;

    CreateMenuItem cmi=CreateMenuItem(cfg);
    string s;
    Menu[1] menu;
    cmi.createWithId!(Locale.Menu.cancel)(&menu[0], "admin:setting");

    rv~="<p>\n<input type=\"hidden\" id=\"salt\" value=\""~Client.getSalt~"\"/>\n"
        "<label>現在のパスワードを入力してください:<input type=\"password\" id=\"password\"/></label><br/>\n"
        "<label>新しいパスワードを入力してください:<input type=\"password\" id=\"newpass\"/></label><br/>\n"
        "<label>再度新しいパスワードを入力してください:<input type=\"password\" id=\"newpass2\"/></label><br/>\n"
        "<input type=\"button\" value=\"変更\" onclick=\"changepass();\"/>\n"
        ~ menu[0].content ~
        "<form action=\""~Http.relPath~"\" method=\"post\" style=\"vibiblity:hidden;\" id=\"exec\">"
        "<input type=\"hidden\" name=\"password\" id=\"sendpass\"/><input type=\"hidden\" name=\"mode\" value=\"admin:updatepass\"/>"
        "</form>\n</p>";
    return rv;
  }
}

ubyte toNum(char c){
  if('0'<=c&&c<='9')return cast(ubyte)(c-'0');
  if('a'<=c&&c<='f')return cast(ubyte)(c-'a'+10);
  if('A'<=c&&c<='F')return cast(ubyte)(c-'A'+10);
  assert(0);
}

char toHex(ubyte c){
  if(0<=c&&c<=9)return cast(char)(c+'0');
  if(10<=c&&c<=15)return cast(char)(c-10+'a');
  assert(0);
}

ubyte[] hexToArr(string str){
  ubyte[] rv;
  rv.length = str.length/2;
  foreach(i, inout a;rv){
    a = cast(ubyte)(toNum(str[i*2]) << 4);
    if(str.length >= i*2+1)
      a |= toNum(str[i*2+1]);
  }
  return rv;
}

string arrToHex(ubyte[] arr){
  char[] rv;
  rv.length = arr.length*2;
  foreach(i, a;arr){
    rv[i*2] = toHex(cast(ubyte)(a >> 4));
    rv[i*2+1] = toHex(cast(ubyte)(a & 15));
  }
  return assumeUnique(rv);
}

T max(T)(T a, T b){
  return a>b?a:b;
}

ubyte[] xor(ubyte[] a, ubyte[] b){
  if(a.length != b.length)
    throw new Exception("");
  ubyte[] rv = new ubyte[a.length];
  for(int i;i<rv.length;i++){
    rv[i] = cast(ubyte)(a[i] ^ b[i]);
  }
  return rv;
}

class UpdatePasswordMode:XhtmlMode!(ModeMethod.POST){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override int permission(){
    return Client.Authority.Admin;
  }
  override string title(){return "パスワードの変更完了";}
  override string contentBody(){//FIXME: チェックが甘い
    string* t="password" in Http.query;
    if(!t)
      throw new SecurityException;
    nazo.file.write("admin.pwd", arrToHex(xor(hexToArr(uridecode(*t)), hexToArr(cfg.passhash))));
    return "<p>パスワードを変更しました。\n<a href=\""~Http.relPath~"?mode=admin:login\">ログイン</a>しなおしてください。</p>";
  }
}

class ShellMode:XhtmlMode!(){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override int permission(){
    return Client.Authority.Admin;
  }
  override string title(){return "シェル";}
  override string contentBody(){
    scripts ~= "shell.js";
    //FIXME: 環境変数(パスを含め)の引き継ぎが不可能
    //FIXME: エラーの表示ができていない
    return "<input id=\"input\" type=\"text\" oninput=\"update(event)\" onkeydown=\"if(event.keyCode==13)do_command()\"/>"
           "<pre id=\"terminal\" style=\"width:40em;height:20em;overflow-y:scroll;white-space: pre-wrap;border:1px solid black;\">"
           ~exec("echo `whoami`@`uname -n`:`pwd`$\\ ")[0..$-1]~"<span id=\"cmd\"></span>"
           "</pre>";
  }
}

/**
 * 実行モード
 * 安全の為、shell以外からは呼び出せません。
 */
class ExecMode:ContentMode!(ModeMethod.POST){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override int permission(){
    return Client.Authority.Admin;
  }
  override string filename(){
    return "cmd.txt"; // FIXME: 日時などを入れる?
  }
  override string contentType(){
    return "text/plain";
  }
  override string content(){
    string referer = Env.TGet!("HTTP_REFERER")();
    int tmp = referer.find("?");
    if(tmp < 0) throw new SecurityException;
    foreach(x; referer[tmp+1..$].split("&")){
      if(x == "mode=admin:shell")
        goto ok;
    }
    throw new SecurityException;
  ok:
    string* _cmd="cmd" in Http.query;
    string* _pwd="pwd" in Http.query;
    string* _interm="interm" in Http.query;
    if(!_cmd)
      throw new SecurityException;
    string cmd = uridecode(*_cmd);
    if(_pwd)cmd = "cd "~uridecode(*_pwd)~"&&"~cmd;
    if(_interm && *_interm == "true") cmd ~= "\necho `whoami`@`uname -n`:`pwd`$\\ ";
    string rv = exec(cmd);
    return rv[0..$-1];
  }
}
